package run.bottle.app.security.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UrlPathHelper;
import run.bottle.app.exception.AbstractBottleException;
import run.bottle.app.security.context.SecurityContextHolder;
import run.bottle.app.security.handler.AuthenticationFailureHandler;
import run.bottle.app.security.handler.DefaultAuthenticationFailureHandler;
import run.bottle.app.utils.RedisUtil;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 * Abstract 认证过滤器
 *
 * @author Lycheng
 */
@Slf4j
public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter {

    protected final RedisUtil cacheStore;

    protected final AntPathMatcher antPathMatcher;

    private final UrlPathHelper urlPathHelper = new UrlPathHelper();

    protected volatile AuthenticationFailureHandler failureHandler;

    /**
     * Exclude url patterns.
     */
    protected Set<String> excludeUrlPatterns = new HashSet<>(16);

    protected Set<String> urlPatterns = new LinkedHashSet<>();

    protected AbstractAuthenticationFilter(RedisUtil cacheStore) {
        this.cacheStore = cacheStore;
        antPathMatcher = new AntPathMatcher();
    }

    protected abstract void doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            // Check the one-time-token
            if (isSufficientOneTimeToken(request)) {
                filterChain.doFilter(request, response);
                return;
            }

            // Do authenticate
            doAuthenticate(request, response, filterChain);
        } catch (AbstractBottleException e) {
            getFailureHandler().onFailure(request, response, e);
        } finally {
            SecurityContextHolder.clearContext();
        }
    }

    public boolean isSufficientOneTimeToken(HttpServletRequest request) {
        return false;
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        Assert.notNull(request, "Http servlet request must not be null");

        // 检查白名单
        boolean result = excludeUrlPatterns.stream().anyMatch(p -> antPathMatcher.match(p, urlPathHelper.getRequestUri(request)));

        return result || urlPatterns.stream().noneMatch(p -> antPathMatcher.match(p, urlPathHelper.getRequestUri(request)));

    }

    /**
     * 从请求中获取令牌
     *
     * @param request http servlet request must not be null
     * @return token or null
     */
    @Nullable
    protected abstract String getTokenFromRequest(@NonNull HttpServletRequest request);

    /**
     * 从http servlet请求获取令牌。
     *
     * @param request         http servlet request must not be null
     * @param tokenQueryName  token query name must not be blank
     * @param tokenHeaderName token header name must not be blank
     * @return corresponding token
     */
    protected String getTokenFromRequest(@NonNull HttpServletRequest request, @NonNull String tokenQueryName, @NonNull String tokenHeaderName) {
        Assert.notNull(request, "Http servlet request must not be null");
        Assert.hasText(tokenQueryName, "Token query name must not be blank");
        Assert.hasText(tokenHeaderName, "Token header name must not be blank");

        // Get from header
        String accessKey = request.getHeader(tokenHeaderName);

        // Get from param
        if (StringUtils.isEmpty(accessKey)) {
            accessKey = request.getParameter(tokenQueryName);
            log.debug("Got access key from parameter: [{}: {}]", tokenQueryName, accessKey);
        } else {
            log.debug("Got access key from header: [{}: {}]", tokenHeaderName, accessKey);
        }

        return accessKey;
    }

    /**
     * 获取身份验证失败处理程序 (默认: @DefaultAuthenticationFailureHandler)
     *
     * @return authentication failure handler
     */
    @NonNull
    private AuthenticationFailureHandler getFailureHandler() {
        if (failureHandler == null) {
            synchronized (this) {
                if (failureHandler == null) {
                    // 创建默认的身份验证失败处理程序
                    DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler();
                    failureHandler.setProductionEnv(false);

                    this.failureHandler = failureHandler;
                }
            }
        }
        return failureHandler;
    }
}
